﻿/* 
 * Copyright (c) 2011-12 Andrey Bulygin
 * 
 * This file is part of Interceptor.
 * Interceptor is free software: you can redistribute it and/or modify it under the terms of the GNU 
 * Library General Public License as published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * Interceptor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 
 * the GNU Library General Public License for more details. You should have received a copy of the GNU 
 * Library General Public License along with Interceptor. If not, write to the Free Software Foundation, Inc., 59 
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 */

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using Interceptor.Core.Exceptions;
using Interceptor.Core.Objects;

namespace Interceptor.Core
{
	internal class SubtypeFactory: BaseFactory
	{
		static string ITC_FLDNAME = "_interceptor";
		internal static Type MakeSubtypeAndInjectInterceptors(Type parentType, Type itcHolderType)
		{
			var itcHolder = ItcHolderDescriptor.GetDescription(itcHolderType);

			var context = GenerateTypeBuilder(parentType);
			var itcFld = context.Builder.DefineField(ITC_FLDNAME, itcHolderType, FieldAttributes.Private);
			CopyConstructors(context.Builder, parentType, itcFld);

			var prepareActions = new List<LambdaExpression>();
			WrapInterface(itcHolder, context, parentType, itcFld, prepareActions);

			Type type = context.Builder.CreateType();

#if SAVE_TO_FILE
			context.Assembly.Save("asm.dll");
#endif
			return type;
		}

		private static void WrapInterface(ItcHolderDescriptor itcHolder, TypeBuilderContext context, Type parentType, FieldBuilder itcFld, List<LambdaExpression> prepareActions)
		{
			var virtualMethods = parentType.GetMethods()
									.Where(x=>x.IsVirtual)
									.Where(x=>!x.IsFinal)
									.ToArray();
			foreach (var method in virtualMethods)
			{
				WrapMethod(itcHolder, context, method, parentType,null, itcFld, prepareActions,false);
			}
		}

		private static MethodBuilder WrapMethod(ItcHolderDescriptor itcHolder
            , TypeBuilderContext context, MethodInfo method, Type parentType
            , MethodInfo intMethod, FieldBuilder itcFld
			, List<LambdaExpression> prepareActions, bool overrideInterfaceMethod)
		{
            var methodToOverride = intMethod ?? method;
			MethodsMapping itcMethodFindResult = FindInterceptionMethod(itcHolder, parentType, method);
			if (itcMethodFindResult == null)
				return null;

            var methodParameters = methodToOverride.GetParameters();

			var methodAttrs = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig;
			if (overrideInterfaceMethod)
				methodAttrs = methodAttrs | MethodAttributes.NewSlot;

			var newMeth = context.Builder.DefineMethod(
                                      methodToOverride.Name, methodAttrs
                                    , methodToOverride.ReturnType
									, methodParameters.Select(x => x.ParameterType).ToArray());

            if (methodToOverride.IsGenericMethod)
			{
                var genArgs = methodToOverride.GetGenericArguments();
				int pIndex = 0;
				GenericTypeParameterBuilder[] gParamDef = newMeth.DefineGenericParameters(genArgs.Select(x => "T" + ++pIndex).ToArray()).ToArray();
				for (var i = 0; i < genArgs.Length; i++)
				{
					gParamDef[i].SetBaseTypeConstraint(genArgs[i].BaseType);
					gParamDef[i].SetInterfaceConstraints(genArgs[i].GetInterfaces());
				}
			}

			for (var i = 0; i < methodParameters.Length; i++)
			{
				newMeth.DefineParameter(i + 1, methodParameters[i].Attributes, methodParameters[i].Name);
			}

			GenerateDelegateCall(context.Module, parentType, itcFld, newMeth, itcMethodFindResult, prepareActions, overrideInterfaceMethod);

			if (overrideInterfaceMethod)
                context.Builder.DefineMethodOverride(newMeth, methodToOverride);

			return newMeth;
		}

		private static void GenerateDelegateCall(ModuleBuilder module, Type parentType, FieldBuilder itcFld, MethodBuilder newMeth, MethodsMapping methodsMapping, List<LambdaExpression> prepareActions, bool callVirt)
		{
			var delegateType = methodsMapping.InterceptorMethod.ContextDelegateType;

			if (delegateType.IsGenericType)
			{
				delegateType = DefyneGenericDelegateMethod(delegateType, parentType, methodsMapping.TargetMethod);
			}

			var invokerType = InvokerFactory.GenerateInvoker(module, methodsMapping, delegateType, prepareActions, false);
			var invokerTypeInvokeMethod = invokerType.GetMethod("InvokeMethod");
			var invokerTypeDefineInvokeMethod = invokerType.GetMethod("DefineInvokerMethod");
			var invokerTypeCtor = invokerType.GetConstructors()[0];
			var invokerFields = invokerType.GetFields(BindingFlags.Public | BindingFlags.Instance);

			var itcMethod = methodsMapping.InterceptorMethod.Method.IsGenericMethod ? DefyneGenericItcMethod(methodsMapping.InterceptorMethod.TargetType, methodsMapping.InterceptorMethod.Method, parentType, methodsMapping.TargetMethod)
																					: methodsMapping.InterceptorMethod.Method;

			if (invokerType.IsGenericType)
			{
				invokerType = invokerType.MakeGenericType(newMeth.GetGenericArguments());
				invokerTypeInvokeMethod = TypeBuilder.GetMethod(invokerType, invokerTypeInvokeMethod);
				invokerTypeCtor = TypeBuilder.GetConstructor(invokerType, invokerTypeCtor);
				invokerTypeDefineInvokeMethod = TypeBuilder.GetMethod(invokerType, invokerTypeDefineInvokeMethod);
				invokerFields = invokerFields.Select(x => TypeBuilder.GetField(invokerType, x)).ToArray();
			}

			var methIL = newMeth.GetILGenerator();
			methIL.DeclareLocal(invokerType);

			//=== CREATE AUTO INVOKER -> in stack new object
			methIL.Emit(OpCodes.Ldarg_0);
			//methIL.Emit(OpCodes.Ldfld, targetFld);
			foreach (var pm in methodsMapping.ParametersMapping.Where(x => x.ItcParameter.IsBatch && x.IsLinkedItcParameter))
			{
				methIL.EmitLdarg((short)(pm.TargetParameterIndex + 1));
				if (pm.TargetParameter.ParameterType.IsByRef)
					methIL.EmitLdInd(pm.TargetParameter.ParameterType.GetElementType());

			}
			methIL.Emit(OpCodes.Newobj, invokerTypeCtor);
			//========================

			methIL.Emit(OpCodes.Stloc_0);

			methIL.Emit(OpCodes.Ldarg_0);
			methIL.Emit(OpCodes.Ldfld, itcFld);
			methIL.Emit(OpCodes.Ldarg_0);
			//methIL.Emit(OpCodes.Ldfld, targetFld);

			if (methodsMapping.InterceptorMethod.InterceptorType == ItcType.Invoker)
				methIL.Emit(OpCodes.Ldloc_0);

			methIL.Emit(OpCodes.Ldloc_0);
			methIL.Emit(OpCodes.Ldftn, invokerTypeInvokeMethod);
			methIL.Emit(OpCodes.Newobj, delegateType.GetConstructors()[0]);

			if (methodsMapping.InterceptorMethod.InterceptorType == ItcType.Invoker)
			{
				methIL.Emit(OpCodes.Call, invokerTypeDefineInvokeMethod);
				methIL.Emit(OpCodes.Ldloc_0);
			}

			//=======
			int mapIndex = 0;
			while (mapIndex < methodsMapping.ParametersMapping.Length)
			{
				var map = methodsMapping.ParametersMapping[mapIndex];
				int capacity = CalculateCapacity(methodsMapping.ParametersMapping, map);
				if (map.ItcParameter.IsBatch)
				{
					if (map.ItcParameter.ParameterType == ItcParameterType.Ignore)
					{
						methIL.Emit(OpCodes.Ldnull);
					}
					else if (map.ItcParameter.ParameterType == ItcParameterType.Skip)
					{
						methIL.EmitLdc_I4(capacity);
						methIL.Emit(OpCodes.Newobj, map.ItcParameter.ParameterInfo.ParameterType.GetConstructors()[0]);
					}
					else
					{
						PutObjectArrayParametersArrayOntoStack(methIL, methodsMapping.TargetMethod, map.TargetParameterIndex, map.ItcParameter.BatchType, capacity);
						methIL.Emit(OpCodes.Newobj, map.ItcParameter.ParameterInfo.ParameterType.GetConstructors()[0]);
					}
				}
				else
				{
					methIL.EmitLdarg((short)(map.TargetParameterIndex + 1));
					if (map.TargetParameter.ParameterType.IsValueType &&
						!map.ItcParameter.ParameterInfo.ParameterType.IsGenericParameter &&
						!map.ItcParameter.ParameterInfo.ParameterType.IsValueType)
						methIL.Emit(OpCodes.Box, map.TargetParameter.ParameterType);
				}
				mapIndex += capacity != 0 ? capacity : 1;
			};

			methIL.Emit(OpCodes.Callvirt, itcMethod);
			if (methodsMapping.TargetMethod.ReturnType == typeof(void) && itcMethod.ReturnType != typeof(void))
				methIL.Emit(OpCodes.Pop);
			// set out params
			foreach (var paramMap in methodsMapping.ParametersMapping.Where(x => x.ItcParameter.IsBatch && x.IsLinkedItcParameter && x.TargetParameter.ParameterType.IsByRef))
			{
				methIL.EmitLdarg((short)(paramMap.TargetParameterIndex + 1));
				methIL.Emit(OpCodes.Ldloc_0);
				var field = invokerFields.Single(x => x.Name == paramMap.TargetParameter.Name);
				methIL.Emit(OpCodes.Ldfld, field);
				methIL.EmitStInd(field.FieldType);
			}

			methIL.Emit(OpCodes.Ret);
		}
		private static void CopyConstructors(TypeBuilder typeBuilder, Type parentType, FieldBuilder itcFld)
		{
			var parentConstructors = parentType.GetConstructors();
			foreach (var parentConstructor in parentConstructors)
			{
				var ctorParameters = parentConstructor.GetParameters();
				// -- CTOR --
				var ctr = typeBuilder.DefineConstructor(MethodAttributes.Public,
													CallingConventions.Standard
												, ctorParameters.Select(x => x.ParameterType)
													.Concat(new []{itcFld.FieldType}).ToArray());
				var ctrIL = ctr.GetILGenerator();

				ctrIL.EmitLdarg(0);

				for (var i = 0;i<ctorParameters.Length; i++)
					ctrIL.EmitLdarg((short)(i+1));

				ctrIL.Emit(OpCodes.Call, parentConstructor);

				ctrIL.EmitLdarg(0);
				ctrIL.EmitLdarg((short)(ctorParameters.Length + 1));
				ctrIL.Emit(OpCodes.Stfld, itcFld);

				ctrIL.Emit(OpCodes.Ret);
			}
		}

		private static TypeBuilderContext GenerateTypeBuilder(Type parentType)
		{
			var buildContext = new TypeBuilderContext();
			var asmName = new AssemblyName("assembly_" + Helpers.GenerateNewUniqueIdentiricator());

#if SAVE_TO_FILE
			buildContext.Assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave);
			buildContext.Module = buildContext.Assembly.DefineDynamicModule(buildContext.Assembly.GetName().Name, buildContext.Assembly.GetName().Name + ".dll");
#else
			buildContext.Assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
			buildContext.Module = buildContext.Assembly.DefineDynamicModule(buildContext.Assembly.GetName().Name);
#endif

			buildContext.Builder = buildContext.Module.DefineType(
									String.Format("{0}.{1}<autosubtype{2}>", parentType.Namespace, parentType.Name, Helpers.GenerateNewUniqueIdentiricator())
									, TypeAttributes.Public | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit
									, parentType);

			buildContext.Builder.AddInterfaceImplementation(typeof(IAutoInherited));
			return buildContext;
		}

		internal static Delegate CreateConstructorDelegate(Type instanceType, Type ctorDefinition)
		{
			if (!ctorDefinition.IsSubclassOf(typeof(MulticastDelegate)))
				throw new InterceptorValidationException("You should provide a multicas delegate as a generic parameter");
			var invokeMethod = ctorDefinition.GetMethod("Invoke");
			var parameters = invokeMethod.GetParameters();
			var result = invokeMethod.ReturnType;
			if (!result.IsAssignableFrom(instanceType))
				throw new InterceptorValidationException("You should provide delegate with result type which is equal to the type you want to get constructor");
			ConstructorInfo ctorInfo = FindConstructor(instanceType, parameters.Select(x => x.ParameterType).ToArray());

			ParameterExpression[] lambdaParams = 
				parameters
				.Select(x => LambdaExpression.Parameter(x.ParameterType))
				.ToArray();

			return LambdaExpression
					.Lambda(ctorDefinition,LambdaExpression.New(ctorInfo, lambdaParams), lambdaParams)
					.Compile();
		}

		private static ConstructorInfo FindConstructor(Type result, Type[] parameters)
		{
			var ctors = result.GetConstructors();
			foreach(var ctor in ctors)
			{
				if (ctor.GetParameters().Select(p => p.ParameterType)
					.SequenceEqual(parameters))
					return ctor;
			}
			throw new InterceptorValidationException("Constructor with specified parameters not found");
		}
	}
}
